#if UNITY_EDITOR
using System;
using System.Collections;
using System.IO;
using System.Linq;
using System.Text;
using UnityEngine.InputSystem.Utilities;
using UnityEditor;

////TODO: option to allow referencing the original asset rather than embedding it

////TODO: emit indexer directly at toplevel so you can more easily look up actions dynamically

////TODO: put the generated code behind #if that depends on input system

////TODO: suffix map properties with Map or Actions (e.g. "PlayerMap" instead of "Player")

////TODO: unify the generated events so that performed, canceled, and started all go into a single event

////TODO: look up actions and maps by ID rather than by name

////TODO: only generate @something if @ is really needed

////TODO: allow having an unnamed or default-named action set which spills actions directly into the toplevel wrapper

////TODO: add cleanup for ActionEvents

////TODO: protect generated wrapper against modifications made to asset

////TODO: make capitalization consistent in the generated code

////TODO: instead of loading from JSON, generate the structure in code

////REVIEW: allow putting *all* of the data from the inputactions asset into the generated class?

namespace UnityEngine.InputSystem.Editor
{
    /// <summary>
    /// Utility to generate code that makes it easier to work with action sets.
    /// </summary>
    public static class InputActionCodeGenerator
    {
        private const int kSpacesPerIndentLevel = 4;

        private const string kClassExample = @"using namespace UnityEngine;
using UnityEngine.InputSystem;

// Example of using an InputActionMap named ""Player"" from a UnityEngine.MonoBehaviour implementing callback interface.
public class Example : MonoBehaviour, MyActions.IPlayerActions
{
    private MyActions_Actions m_Actions;                  // Source code representation of asset.
    private MyActions_Actions.PlayerActions m_Player;     // Source code representation of action map.

    void Awake()
    {
        m_Actions = new MyActions_Actions();              // Create asset object.
        m_Player = m_Actions.Player;                      // Extract action map object.
        m_Player.AddCallbacks(this);                      // Register callback interface IPlayerActions.
    }

    void OnDestroy()
    {
        m_Actions.Dispose();                              // Destroy asset object.
    }

    void OnEnable()
    {
        m_Player.Enable();                                // Enable all actions within map.
    }

    void OnDisable()
    {
        m_Player.Disable();                               // Disable all actions within map.
    }

    #region Interface implementation of MyActions.IPlayerActions

    // Invoked when ""Move"" action is either started, performed or canceled.
    public void OnMove(InputAction.CallbackContext context)
    {
        Debug.Log($""OnMove: {context.ReadValue<Vector2>()}"");
    }

    // Invoked when ""Attack"" action is either started, performed or canceled.
    public void OnAttack(InputAction.CallbackContext context)
    {
        Debug.Log($""OnAttack: {context.ReadValue<float>()}"");
    }

    #endregion
}";

        public struct Options
        {
            public string className { get; set; }
            public string namespaceName { get; set; }
            public string sourceAssetPath { get; set; }
        }

        public static string GenerateWrapperCode(InputActionAsset asset, Options options = default)
        {
            if (asset == null)
                throw new ArgumentNullException(nameof(asset));

            if (string.IsNullOrEmpty(options.sourceAssetPath))
                options.sourceAssetPath = AssetDatabase.GetAssetPath(asset);
            if (string.IsNullOrEmpty(options.className) && !string.IsNullOrEmpty(asset.name))
                options.className =
                    CSharpCodeHelpers.MakeTypeName(asset.name);

            if (string.IsNullOrEmpty(options.className))
            {
                if (string.IsNullOrEmpty(options.sourceAssetPath))
                    throw new ArgumentException("options.sourceAssetPath");
                options.className =
                    CSharpCodeHelpers.MakeTypeName(Path.GetFileNameWithoutExtension(options.sourceAssetPath));
            }

            var writer = new Writer
            {
                buffer = new StringBuilder()
            };

            // Header.
            writer.WriteLine(CSharpCodeHelpers.MakeAutoGeneratedCodeHeader("com.unity.inputsystem:InputActionCodeGenerator",
                InputSystem.version.ToString(),
                options.sourceAssetPath));

            // Usings.
            writer.WriteLine("using System;");
            writer.WriteLine("using System.Collections;");
            writer.WriteLine("using System.Collections.Generic;");
            writer.WriteLine("using UnityEngine.InputSystem;");
            writer.WriteLine("using UnityEngine.InputSystem.Utilities;");
            writer.WriteLine("");

            // Begin namespace.
            var haveNamespace = !string.IsNullOrEmpty(options.namespaceName);
            if (haveNamespace)
            {
                writer.WriteLine($"namespace {options.namespaceName}");
                writer.BeginBlock();
            }

            // Begin class.
            writer.DocSummary($"Provides programmatic access to <see cref=\"InputActionAsset\" />, " +
                "<see cref=\"InputActionMap\" />, <see cref=\"InputAction\" /> and " +
                "<see cref=\"InputControlScheme\" /> instances defined " +
                $"in asset \"{options.sourceAssetPath}\".");
            writer.DocRemarks("This class is source generated and any manual edits will be discarded if the associated asset is reimported or modified.");
            writer.DocExample(kClassExample);

            writer.WriteLine($"public partial class @{options.className}: IInputActionCollection2, IDisposable");
            writer.BeginBlock();

            writer.DocSummary("Provides access to the underlying asset instance.");
            writer.WriteLine($"public InputActionAsset asset {{ get; }}");
            writer.WriteLine();

            // Default constructor.
            writer.DocSummary("Constructs a new instance.");
            writer.WriteLine($"public @{options.className}()");
            writer.BeginBlock();
            writer.WriteLine($"asset = InputActionAsset.FromJson(@\"{asset.ToJson().Replace("\"", "\"\"")}\");");

            var maps = asset.actionMaps;
            var schemes = asset.controlSchemes;
            foreach (var map in maps)
            {
                var mapName = CSharpCodeHelpers.MakeIdentifier(map.name);
                writer.WriteLine($"// {map.name}");
                writer.WriteLine($"m_{mapName} = asset.FindActionMap(\"{map.name}\", throwIfNotFound: true);");

                foreach (var action in map.actions)
                {
                    var actionName = CSharpCodeHelpers.MakeIdentifier(action.name);
                    writer.WriteLine($"m_{mapName}_{actionName} = m_{mapName}.FindAction(\"{action.name}\", throwIfNotFound: true);");
                }
            }
            writer.EndBlock();
            writer.WriteLine();

            writer.WriteLine($"~@{options.className}()");
            writer.BeginBlock();
            foreach (var map in maps)
            {
                var mapName = CSharpCodeHelpers.MakeIdentifier(map.name);
                writer.WriteLine($"UnityEngine.Debug.Assert(!m_{mapName}.enabled, \"This will cause a leak and performance issues, {options.className}.{mapName}.Disable() has not been called.\");");
            }
            writer.EndBlock();
            writer.WriteLine();

            writer.DocSummary("Destroys this asset and all associated <see cref=\"InputAction\"/> instances.");
            writer.WriteLine("public void Dispose()");
            writer.BeginBlock();
            writer.WriteLine("UnityEngine.Object.Destroy(asset);");
            writer.EndBlock();
            writer.WriteLine();

            var classNamePrefix = typeof(InputActionAsset).Namespace + "." + nameof(InputActionAsset) + ".";
            writer.DocInherit(classNamePrefix + nameof(InputActionAsset.bindingMask));
            writer.WriteLine("public InputBinding? bindingMask");
            writer.BeginBlock();
            writer.WriteLine("get => asset.bindingMask;");
            writer.WriteLine("set => asset.bindingMask = value;");
            writer.EndBlock();
            writer.WriteLine();

            writer.DocInherit(classNamePrefix + nameof(InputActionAsset.devices));
            writer.WriteLine("public ReadOnlyArray<InputDevice>? devices");
            writer.BeginBlock();
            writer.WriteLine("get => asset.devices;");
            writer.WriteLine("set => asset.devices = value;");
            writer.EndBlock();
            writer.WriteLine();

            writer.DocInherit(classNamePrefix + nameof(InputActionAsset.controlSchemes));
            writer.WriteLine("public ReadOnlyArray<InputControlScheme> controlSchemes => asset.controlSchemes;");
            writer.WriteLine();

            writer.DocInherit(classNamePrefix + nameof(InputActionAsset.Contains) + "(InputAction)");
            writer.WriteLine("public bool Contains(InputAction action)");
            writer.BeginBlock();
            writer.WriteLine("return asset.Contains(action);");
            writer.EndBlock();
            writer.WriteLine();

            writer.DocInherit(classNamePrefix + nameof(InputActionAsset.GetEnumerator) + "()");
            writer.WriteLine("public IEnumerator<InputAction> GetEnumerator()");
            writer.BeginBlock();
            writer.WriteLine("return asset.GetEnumerator();");
            writer.EndBlock();
            writer.WriteLine();

            writer.DocInherit(nameof(IEnumerable) + "." + nameof(IEnumerable.GetEnumerator) + "()");
            writer.WriteLine("IEnumerator IEnumerable.GetEnumerator()");
            writer.BeginBlock();
            writer.WriteLine("return GetEnumerator();");
            writer.EndBlock();
            writer.WriteLine();

            writer.DocInherit(classNamePrefix + nameof(InputActionAsset.Enable) + "()");
            writer.WriteLine("public void Enable()");
            writer.BeginBlock();
            writer.WriteLine("asset.Enable();");
            writer.EndBlock();
            writer.WriteLine();

            writer.DocInherit(classNamePrefix + nameof(InputActionAsset.Disable) + "()");
            writer.WriteLine("public void Disable()");
            writer.BeginBlock();
            writer.WriteLine("asset.Disable();");
            writer.EndBlock();
            writer.WriteLine();

            writer.DocInherit(classNamePrefix + nameof(InputActionAsset.bindings));
            writer.WriteLine("public IEnumerable<InputBinding> bindings => asset.bindings;");
            writer.WriteLine();

            writer.DocInherit(classNamePrefix + nameof(InputActionAsset.FindAction) + "(string, bool)");
            writer.WriteLine("public InputAction FindAction(string actionNameOrId, bool throwIfNotFound = false)");
            writer.BeginBlock();
            writer.WriteLine("return asset.FindAction(actionNameOrId, throwIfNotFound);");
            writer.EndBlock();
            writer.WriteLine();

            writer.DocInherit(classNamePrefix + nameof(InputActionAsset.FindBinding) + "(InputBinding, out InputAction)");
            writer.WriteLine("public int FindBinding(InputBinding bindingMask, out InputAction action)");
            writer.BeginBlock();
            writer.WriteLine("return asset.FindBinding(bindingMask, out action);");
            writer.EndBlock();

            // Action map accessors.
            var inputActionMapClassPrefix = typeof(InputActionMap).Namespace + "." + nameof(InputActionMap) + ".";
            foreach (var map in maps)
            {
                writer.WriteLine();
                writer.WriteLine($"// {map.name}");

                var mapName = CSharpCodeHelpers.MakeIdentifier(map.name);
                var mapTypeName = CSharpCodeHelpers.MakeTypeName(mapName, "Actions");

                // Caching field for action map.
                writer.WriteLine($"private readonly InputActionMap m_{mapName};");
                writer.WriteLine(string.Format("private List<I{0}> m_{0}CallbackInterfaces = new List<I{0}>();", mapTypeName));

                // Caching fields for all actions.
                foreach (var action in map.actions)
                {
                    var actionName = CSharpCodeHelpers.MakeIdentifier(action.name);
                    writer.WriteLine($"private readonly InputAction m_{mapName}_{actionName};");
                }

                // Struct wrapping access to action set.
                writer.DocSummary($"Provides access to input actions defined in input action map \"{map.name}\".");
                writer.WriteLine($"public struct {mapTypeName}");
                writer.BeginBlock();

                writer.WriteLine($"private @{options.className} m_Wrapper;");
                writer.WriteLine();

                // Constructor.
                writer.DocSummary("Construct a new instance of the input action map wrapper class.");
                writer.WriteLine($"public {mapTypeName}(@{options.className} wrapper) {{ m_Wrapper = wrapper; }}");

                // Getter for each action.
                foreach (var action in map.actions)
                {
                    var actionName = CSharpCodeHelpers.MakeIdentifier(action.name);
                    writer.DocSummary($"Provides access to the underlying input action \"{mapName}/{actionName}\".");
                    writer.WriteLine(
                        $"public InputAction @{actionName} => m_Wrapper.m_{mapName}_{actionName};");
                }

                // Action map getter.
                writer.DocSummary("Provides access to the underlying input action map instance.");
                writer.WriteLine($"public InputActionMap Get() {{ return m_Wrapper.m_{mapName}; }}");

                // Enable/disable methods.
                writer.DocInherit(inputActionMapClassPrefix + nameof(InputActionMap.Enable) + "()");
                writer.WriteLine("public void Enable() { Get().Enable(); }");
                writer.DocInherit(inputActionMapClassPrefix + nameof(InputActionMap.Disable) + "()");
                writer.WriteLine("public void Disable() { Get().Disable(); }");
                writer.DocInherit(inputActionMapClassPrefix + nameof(InputActionMap.enabled));
                writer.WriteLine("public bool enabled => Get().enabled;");

                // Implicit conversion operator.
                writer.DocSummary($"Implicitly converts an <see ref=\"{mapTypeName}\" /> to an <see ref=\"InputActionMap\" /> instance.");
                writer.WriteLine(
                    $"public static implicit operator InputActionMap({mapTypeName} set) {{ return set.Get(); }}");

                // AddCallbacks method.
                writer.DocSummary("Adds <see cref=\"InputAction.started\"/>, <see cref=\"InputAction.performed\"/> and <see cref=\"InputAction.canceled\"/> callbacks provided via <param cref=\"instance\" /> on all input actions contained in this map.");
                writer.DocParam("instance", "Callback instance.");
                writer.DocRemarks("If <paramref name=\"instance\" /> is <c>null</c> or <paramref name=\"instance\"/> have already been added this method does nothing.");
                writer.DocSeeAlso(mapTypeName);
                writer.WriteLine($"public void AddCallbacks(I{mapTypeName} instance)");
                writer.BeginBlock();

                // Initialize new interface.
                writer.WriteLine($"if (instance == null || m_Wrapper.m_{mapTypeName}CallbackInterfaces.Contains(instance)) return;");
                writer.WriteLine($"m_Wrapper.m_{mapTypeName}CallbackInterfaces.Add(instance);");

                foreach (var action in map.actions)
                {
                    var actionName = CSharpCodeHelpers.MakeIdentifier(action.name);
                    var actionTypeName = CSharpCodeHelpers.MakeTypeName(action.name);

                    writer.WriteLine($"@{actionName}.started += instance.On{actionTypeName};");
                    writer.WriteLine($"@{actionName}.performed += instance.On{actionTypeName};");
                    writer.WriteLine($"@{actionName}.canceled += instance.On{actionTypeName};");
                }

                writer.EndBlock();
                writer.WriteLine();

                // UnregisterCallbacks method.
                writer.DocSummary("Removes <see cref=\"InputAction.started\"/>, <see cref=\"InputAction.performed\"/> and <see cref=\"InputAction.canceled\"/> callbacks provided via <param cref=\"instance\" /> on all input actions contained in this map.");
                writer.DocRemarks("Calling this method when <paramref name=\"instance\" /> have not previously been registered has no side-effects.");
                writer.DocSeeAlso(mapTypeName);
                writer.WriteLine($"private void UnregisterCallbacks(I{mapTypeName} instance)");
                writer.BeginBlock();
                foreach (var action in map.actions)
                {
                    var actionName = CSharpCodeHelpers.MakeIdentifier(action.name);
                    var actionTypeName = CSharpCodeHelpers.MakeTypeName(action.name);

                    writer.WriteLine($"@{actionName}.started -= instance.On{actionTypeName};");
                    writer.WriteLine($"@{actionName}.performed -= instance.On{actionTypeName};");
                    writer.WriteLine($"@{actionName}.canceled -= instance.On{actionTypeName};");
                }
                writer.EndBlock();
                writer.WriteLine();

                // RemoveCallbacks method.
                writer.DocSummary($"Unregisters <param cref=\"instance\" /> and unregisters all input action callbacks via <see cref=\"{mapTypeName}.UnregisterCallbacks(I{mapTypeName})\" />.");
                writer.DocSeeAlso($"{mapTypeName}.UnregisterCallbacks(I{mapTypeName})");
                writer.WriteLine($"public void RemoveCallbacks(I{mapTypeName} instance)");
                writer.BeginBlock();
                writer.WriteLine($"if (m_Wrapper.m_{mapTypeName}CallbackInterfaces.Remove(instance))");
                writer.WriteLine($"    UnregisterCallbacks(instance);");
                writer.EndBlock();
                writer.WriteLine();

                // SetCallbacks method.
                writer.DocSummary($"Replaces all existing callback instances and previously registered input action callbacks associated with them with callbacks provided via <param cref=\"instance\" />.");
                writer.DocRemarks($"If <paramref name=\"instance\" /> is <c>null</c>, calling this method will only unregister all existing callbacks but not register any new callbacks.");
                writer.DocSeeAlso($"{mapTypeName}.AddCallbacks(I{mapTypeName})");
                writer.DocSeeAlso($"{mapTypeName}.RemoveCallbacks(I{mapTypeName})");
                writer.DocSeeAlso($"{mapTypeName}.UnregisterCallbacks(I{mapTypeName})");
                writer.WriteLine($"public void SetCallbacks(I{mapTypeName} instance)");
                writer.BeginBlock();
                ////REVIEW: this would benefit from having a single callback on InputActions rather than three different endpoints

                writer.WriteLine($"foreach (var item in m_Wrapper.m_{mapTypeName}CallbackInterfaces)");
                writer.WriteLine($"    UnregisterCallbacks(item);");
                writer.WriteLine($"m_Wrapper.m_{mapTypeName}CallbackInterfaces.Clear();");

                // Initialize new interface.
                writer.WriteLine("AddCallbacks(instance);");
                writer.EndBlock();
                writer.EndBlock();

                // Getter for instance of struct.
                writer.DocSummary($"Provides a new <see cref=\"{mapTypeName}\" /> instance referencing this action map.");
                writer.WriteLine($"public {mapTypeName} @{mapName} => new {mapTypeName}(this);");
            }

            // Control scheme accessors.
            foreach (var scheme in schemes)
            {
                var identifier = CSharpCodeHelpers.MakeIdentifier(scheme.name);

                writer.WriteLine($"private int m_{identifier}SchemeIndex = -1;");
                writer.DocSummary("Provides access to the input control scheme.");
                writer.DocSeeAlso(typeof(InputControlScheme).Namespace + "." + nameof(InputControlScheme));
                writer.WriteLine($"public InputControlScheme {identifier}Scheme");
                writer.BeginBlock();
                writer.WriteLine("get");
                writer.BeginBlock();
                writer.WriteLine($"if (m_{identifier}SchemeIndex == -1) m_{identifier}SchemeIndex = asset.FindControlSchemeIndex(\"{scheme.name}\");");
                writer.WriteLine($"return asset.controlSchemes[m_{identifier}SchemeIndex];");
                writer.EndBlock();
                writer.EndBlock();
            }

            // Generate interfaces.
            var inputActionClassReference = typeof(InputAction).Namespace + "." + nameof(InputAction) + ".";
            foreach (var map in maps)
            {
                var typeName = CSharpCodeHelpers.MakeTypeName(map.name);
                writer.DocSummary($"Interface to implement callback methods for all input action callbacks associated with input actions defined by \"{map.name}\" which allows adding and removing callbacks.");
                writer.DocSeeAlso($"{typeName}Actions.AddCallbacks(I{typeName}Actions)");
                writer.DocSeeAlso($"{typeName}Actions.RemoveCallbacks(I{typeName}Actions)");
                writer.WriteLine($"public interface I{typeName}Actions");
                writer.BeginBlock();

                foreach (var action in map.actions)
                {
                    var methodName = CSharpCodeHelpers.MakeTypeName(action.name);
                    writer.DocSummary($"Method invoked when associated input action \"{action.name}\" is either <see cref=\"UnityEngine.InputSystem.InputAction.started\" />, <see cref=\"UnityEngine.InputSystem.InputAction.performed\" /> or <see cref=\"UnityEngine.InputSystem.InputAction.canceled\" />.");
                    writer.DocSeeAlso(string.Concat(inputActionClassReference, "started"));
                    writer.DocSeeAlso(string.Concat(inputActionClassReference, "performed"));
                    writer.DocSeeAlso(string.Concat(inputActionClassReference, "canceled"));
                    writer.WriteLine($"void On{methodName}(InputAction.CallbackContext context);");
                }

                writer.EndBlock();
            }

            // End class.
            writer.EndBlock();

            // End namespace.
            if (haveNamespace)
                writer.EndBlock();

            return writer.buffer.ToString();
        }

        ////TODO: move this to a shared place
        internal struct Writer
        {
            public StringBuilder buffer;
            public int indentLevel;

            public void BeginBlock()
            {
                WriteIndent();
                buffer.Append("{\n");
                ++indentLevel;
            }

            public void EndBlock()
            {
                --indentLevel;
                WriteIndent();
                buffer.Append("}\n");
            }

            public void WriteLine()
            {
                buffer.Append('\n');
            }

            public void WriteLine(string text)
            {
                if (!text.All(char.IsWhiteSpace))
                {
                    WriteIndent();
                    buffer.Append(text);
                }
                buffer.Append('\n');
            }

            public void Write(string text)
            {
                buffer.Append(text);
            }

            public void WriteIndent()
            {
                for (var i = 0; i < indentLevel; ++i)
                {
                    for (var n = 0; n < kSpacesPerIndentLevel; ++n)
                        buffer.Append(' ');
                }
            }

            public void DocSummary(string text)
            {
                DocElement("summary", text);
            }

            public void DocParam(string paramName, string text)
            {
                WriteLine($"/// <param name=\"{paramName}\">{text}</param>");
            }

            public void DocRemarks(string text)
            {
                DocElement("remarks", text);
            }

            public void DocInherit(string cref)
            {
                DocReference("inheritdoc", cref);
            }

            public void DocSeeAlso(string cref)
            {
                DocReference("seealso", cref: cref);
            }

            public void DocExample(string code)
            {
                DocComment("<example>");
                DocComment("<code>");

                foreach (var line in code.Split('\n'))
                    DocComment(line.Replace("<", "&lt;").Replace(">", "&gt;"));

                DocComment("</code>");
                DocComment("</example>");
            }

            private void DocComment(string text)
            {
                if (string.IsNullOrEmpty(text))
                    WriteLine("///");
                else
                    WriteLine(string.Concat("/// ", text));
            }

            private void DocElement(string tag, string text)
            {
                DocComment($"<{tag}>");
                DocComment(text);
                DocComment($"</{tag}>");
            }

            private void DocReference(string tag, string cref)
            {
                DocInlineElement(tag, "cref", cref);
            }

            private void DocInlineElement(string tag, string property, string value)
            {
                DocComment($"<{tag} {property}=\"{value}\" />");
            }
        }

        // Updates the given file with wrapper code generated for the given action sets.
        // If the generated code is unchanged, does not touch the file.
        // Returns true if the file was touched, false otherwise.
        public static bool GenerateWrapperCode(string filePath, InputActionAsset asset, Options options)
        {
            if (!Path.HasExtension(filePath))
                filePath += ".cs";

            // Generate code.
            var code = GenerateWrapperCode(asset, options);

            // Check if the code changed. Don't write if it hasn't.
            if (File.Exists(filePath))
            {
                var existingCode = File.ReadAllText(filePath);
                if (existingCode == code || existingCode.WithAllWhitespaceStripped() == code.WithAllWhitespaceStripped())
                    return false;
            }

            // Write.
            EditorHelpers.CheckOut(filePath);
            File.WriteAllText(filePath, code);
            return true;
        }
    }
}
#endif // UNITY_EDITOR
